using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Picnic.Spec.Utils;

namespace Picnic.Spec
{
	/// <summary>
	/// Represents a specification manager.
	/// </summary>
	public class SpecificationManager
	{
		private readonly List<Specification> _specifications = new List<Specification>();

		/// <summary>
		/// Adds a specification.
		/// </summary>
		public void Add(Type specificationType)
		{
			Ensure.That(() => specificationType).IsNotNull();

			if (!specificationType.IsSubclassOf(typeof(Specification)))
			{
				throw new ArgumentException(string.Format(
					"Specification type should be a subclass of {0}.",
					typeof(Specification)));
			}

			var specifiaction = (Specification)Activator.CreateInstance(specificationType);
			_specifications.Add(specifiaction);
		}

		/// <summary>
		/// Adds a specifications.
		/// </summary>
		public void Add(IEnumerable<Type> specificationTypes)
		{
			foreach (var specificationType in specificationTypes)
			{
				Add(specificationType);
			}
		}

		/// <summary>
		/// Returns statement scopes from contained specifications.
		/// </summary>
		public IEnumerable<StatementScope> GetStatementScopes()
		{
			return _specifications.SelectMany(GetStatementsSafe);
		}

		/// <summary>
		/// Validates managed statements.
		/// </summary>
		public void ValidateStatements(ISpecificationValidationListener listener)
		{
			var satisfiedStatementCount = 0;
			var passedStatementsCount = 0;
			var failedStatementCount = 0;
			var stopwatch = new Stopwatch();
			var scopes = GetStatementScopes().OrderBy(s => s.Subject + s.Context);

			stopwatch.Start();
			listener.BeginValidation();
			foreach (var scope in scopes)
			{
				listener.BeginStatementScopeValidation(scope);

				var validationResult = ValidateScope(listener, scope);
				satisfiedStatementCount += validationResult.SatisfiedStatementCount;
				passedStatementsCount += validationResult.PassedStatementCount;
				failedStatementCount += validationResult.FailedStatementCount;

				listener.EndStatementScopeValidation(scope, validationResult);
			}
			stopwatch.Stop();

			var elapsed = stopwatch.Elapsed;
			var result = new StatementScopeValidationResult(
				elapsed,
				satisfiedStatementCount,
				passedStatementsCount,
				failedStatementCount);

			listener.EndValidation(result);
		}

		private static StatementScopeValidationResult ValidateScope(
			ISpecificationValidationListener listener,
			StatementScope scope)
		{
			var satisfiedStatementCount = 0;
			var passedStatementsCount = 0;
			var failedStatementCount = 0;
			var stopwatch = new Stopwatch();

			stopwatch.Start();
			foreach (var statement in scope.Statements)
			{
				listener.BeginStatementValidation(statement);

				var validationResult = ValidateStatement(statement);
				switch (validationResult.Kind)
				{
					case StatementValidationResultKind.Satisfied:
						satisfiedStatementCount++;
						break;
					case StatementValidationResultKind.Failed:
						failedStatementCount++;
						break;
					default:
						passedStatementsCount++;
						break;
				}

				listener.EndStatementValidation(statement, validationResult);
			}
			stopwatch.Stop();

			var elapsed = stopwatch.Elapsed;
			var result = new StatementScopeValidationResult(
				elapsed,
				satisfiedStatementCount,
				passedStatementsCount,
				failedStatementCount);

			return result;
		}

		private static StatementValidationResult ValidateStatement(Statement statement)
		{
			Exception error = null;
			var stopwatch = new Stopwatch();

			stopwatch.Start();
			try
			{
				if (statement.Before != null)
				{
					statement.Before();
				}
				statement.Body();
			}
			catch (Exception statementError)
			{
				error = statementError;
			}
			stopwatch.Stop();

			var elapsed = stopwatch.Elapsed;
			var result = new StatementValidationResult(elapsed, error);
			return result;
		}

		private static IEnumerable<StatementScope> GetStatementsSafe(Specification specification)
		{
			try
			{
				return specification.GetStatementScopes();
			}
			catch(Exception error)
			{
				var subject = "Specification \"" + specification.GetType().FullName + "\"";
				var statement = new Statement("should not raise execeptions", () => { throw error; });
				var scope = new StatementScope(subject, "initialization", new [] { statement });
				return new[] { scope };
			}
		}
	}
}